namespace Tekdev.Cryptography
{
	using System;
	using System.IO;
	using System.Text;
	using System.Security.Cryptography;

	public class Hash
	{
		#region Create Hash Methods
		public static string Create(string expression)
		{
			return Create(expression, null, HashType.SHA512);
		}

		public static string Create(string expression, HashType hash)
		{
			return Create(expression, null, hash);
		}

		public static string Create(string expression, string salt, HashType hash)
		{           
			byte[] saltBytes;

			// If salt is not specified, generate it on the fly.
			if (salt == null || salt == string.Empty)
			{
				// Define min and max salt sizes.
				int minSaltSize = 4;
				int maxSaltSize = 8;

				// Generate a random number for the size of the salt.
				Random  random = new Random();
				int saltSize = random.Next(minSaltSize, maxSaltSize);

				// Allocate a byte array, which will hold the salt.
				saltBytes = new byte[saltSize];

				// Initialize a random number generator.
				RNGCryptoServiceProvider rng = new RNGCryptoServiceProvider();

				// Fill the salt with cryptographically strong byte values.
				rng.GetNonZeroBytes(saltBytes); 
			}
			else
			{
				saltBytes = Encoding.UTF8.GetBytes(salt);
			}				
        
			// Convert plain text into a byte array.
			byte[] plainTextBytes = Encoding.UTF8.GetBytes(expression);
        
			// Allocate array, which will hold plain text and salt.
			byte[] plainTextWithSaltBytes = 
				new byte[plainTextBytes.Length + saltBytes.Length];

			// Copy plain text bytes into resulting array.
			for (int i=0; i < plainTextBytes.Length; i++)
			{
				plainTextWithSaltBytes[i] = plainTextBytes[i];
			}
        
			// Append salt bytes to the resulting array.
			for (int i=0; i < saltBytes.Length; i++)
			{
				plainTextWithSaltBytes[plainTextBytes.Length + i] = saltBytes[i];
			}

			// Because we support multiple hashing algorithms, we must define
			// hash object as a common (abstract) base class. We will specify the
			// actual hashing algorithm class later during object creation.
			HashAlgorithm hashType;
                
			// Initialize appropriate hashing algorithm class.
			switch (hash)
			{
				case HashType.SHA1:
					hashType = new SHA1Managed();
					break;

				case HashType.SHA256:
					hashType = new SHA256Managed();
					break;

				case HashType.SHA384:
					hashType = new SHA384Managed();
					break;

				case HashType.SHA512:
					hashType = new SHA512Managed();
					break;

				default:
					hashType = new MD5CryptoServiceProvider();
					break;
			}
        
			// Compute hash value of our plain text with appended salt.
			byte[] hashBytes = hashType.ComputeHash(plainTextWithSaltBytes);
        
			// Create array which will hold hash and original salt bytes.
			byte[] hashWithSaltBytes = new byte[hashBytes.Length + saltBytes.Length];
        
			// Copy hash bytes into resulting array.
			for (int i=0; i < hashBytes.Length; i++)
			{
				hashWithSaltBytes[i] = hashBytes[i];
			}
            
			// Append salt bytes to the result.
			for (int i=0; i < saltBytes.Length; i++)
			{
				hashWithSaltBytes[hashBytes.Length + i] = saltBytes[i];
			}
            
			// Convert result into a base64-encoded string.
			string hashValue = Convert.ToBase64String(hashWithSaltBytes);
        
			// Return the result.
			return hashValue;
		}
		#endregion

		#region Validate Hash Methods
		public static bool Verify(string expression, string hashValue)
		{
			return Verify(expression, hashValue, HashType.SHA512);
		}

		public static bool Verify(string expression, string hashValue, HashType hash)
		{                                 
			// Convert base64-encoded hash value into a byte array.
			byte[] hashWithSaltBytes = Convert.FromBase64String(hashValue);
        
			// We must know size of hash (without salt).
			int hashSizeInBits, hashSizeInBytes;
        
			// Size of hash is based on the specified algorithm.
			switch (hash)
			{
				case HashType.SHA1:
					hashSizeInBits = 160;
					break;

				case HashType.SHA256:
					hashSizeInBits = 256;
					break;

				case HashType.SHA384:
					hashSizeInBits = 384;
					break;

				case HashType.SHA512:
					hashSizeInBits = 512;
					break;

				default: // Must be MD5
					hashSizeInBits = 128;
					break;
			}

			// Convert size of hash from bits to bytes.
			hashSizeInBytes = hashSizeInBits / 8;

			// Make sure that the specified hash value is long enough.
			if (hashWithSaltBytes.Length < hashSizeInBytes)
			{
				return false;
			}

			// Allocate array to hold original salt bytes retrieved from hash.
			byte[] saltBytes = new byte[hashWithSaltBytes.Length - 
				hashSizeInBytes];

			// Copy salt from the end of the hash to the new array.
			for (int i=0; i < saltBytes.Length; i++)
				saltBytes[i] = hashWithSaltBytes[hashSizeInBytes + i];

			// Compute a new hash string.
			string expectedHashString = Create(expression, hash);

			// If the computed hash matches the specified hash,
			// the plain text value must be correct.
			return (hashValue == expectedHashString);
		}
		#endregion
	}
}
